Skip to main content

Object-Oriented Abuser

Semua jenis Object-Oriented Abusers, penjelasan, contoh kode nyata, dan cara refactoringnya

Code smell ini terjadi karena penerapan prinsip-prinsip OOP yang salah, tidak lengkap, atau dipaksakan.

Sumber: Refactoring Guru


Switch Statements

Kamu punya blok switch yang kompleks atau rangkaian if-else panjang untuk menangani berbagai tipe/varian.

Masalah

Setiap kali ada tipe baru, kamu harus mengedit semua class yang punya if-else tersebut. Ini melanggar Open/Closed Principle — code harusnya terbuka untuk ekstensi, tertutup untuk modifikasi.

Contoh nyata: ShapePrinter dan CharNeededCounter punya if-else identik untuk tipe shape. Tambah shape Circle? Harus edit dua class sekaligus.

Explorer
switch_statements
before
ShapePrinter.java
CharNeededCounter.java
Main.java
after
Shape.java
Square.java
Triangle.java
ShapeFactory.java
Main.java
switch_statements/before/ShapePrinter.java
package oo_abusers.switch_statements.before;

public class ShapePrinter {
public void print(String shape, int size){
if(shape.equalsIgnoreCase("square")){
for(int i = 0; i < size; i++){
for(int j = 0; j < size; j++){
System.out.print("*");
}
System.out.println("");
}
} else if(shape.equalsIgnoreCase("triangle")){
for(int i = 1; i <= size; i++){
for(int j = 0; j < i; j++){
System.out.print("*");
}
System.out.println("");
}
} else {
System.out.println("invalid shape");
}
}
}

Solusi

Ubah setiap tipe shape menjadi class yang mewarisi abstract class Shape. Logika print() dan charNeeded() dipindahkan ke masing-masing class.

Sebelum — if-else tersebar di ShapePrinter dan CharNeededCounter
// if-else yang sama ada di ShapePrinter DAN CharNeededCounter!
public class ShapePrinter {
public void print(String shape, int size) {
if (shape.equalsIgnoreCase("square")) {
// logika cetak kotak...
} else if (shape.equalsIgnoreCase("triangle")) {
// logika cetak segitiga...
}
}
}

public class CharNeededCounter {
public int count(String shape, int size) {
if (shape.equalsIgnoreCase("square")) return size * size;
else if (shape.equalsIgnoreCase("triangle")) return ((size+1)*size)/2;
return -1;
}
}
Sesudah — setiap shape punya class sendiri
// Tiap shape urus diri sendiri
public abstract class Shape {
protected int size;
public abstract void print();
public abstract int charNeeded();
}

public class Square extends Shape {
@Override public void print() { /* logika kotak */ }
@Override public int charNeeded() { return size * size; }
}

// Tambah Circle? Cukup buat class baru — tidak ada class lain yang diubah!
public class Circle extends Shape {
@Override public void print() { /* logika lingkaran */ }
@Override public int charNeeded() { /* rumus lingkaran */ }
}

Perbandingan

FiturSebelumSesudah
KeterbacaanBlok if-else besar menyembunyikan tujuanSetiap class punya satu tanggung jawab
PengujianHarus test semua branch dalam satu methodSetiap shape ditest secara mandiri
PemeliharaanTambah shape baru = edit banyak fileBuat class baru, tidak ada yang diubah
ReusabilitasLogika terkurung dalam methodObjek shape bisa dioper ke mana saja

Temporary Field

Field di dalam class hanya diisi nilainya pada kondisi tertentu — "numpang lewat" saja.

Masalah

Field distance, fuelCost, weightFee di CodeSmell_ShippingOrder hanya dipakai saat kalkulasi. Di method lain, field ini tidak dipakai sama sekali — tapi tetap ada dan membingungkan pembaca.

Explorer
temporary_field
before
CodeSmell_ShippingOrder.java
after
CodeSmell_ShippingOrder.java
CalculateShippingOrder.java
temporary_field/before/CodeSmell_ShippingOrder.java
package oo_abusers.temporary_field.before;

public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;

// --- TEMPORARY FIELDS ---
// Variabel ini cuma dipakai buat hitung-hitungan sementara
private double distance;
private double fuelCost;
private double weightFee;

public CodeSmell_ShippingOrder(String destination, double basePrice) {
this.destination = destination;
this.basePrice = basePrice;
}

public double calculateTotal(double weight, double km) {
// Mengisi temporary fields
distance = km;
fuelCost = distance * 2000;
weightFee = weight * 5000;

return basePrice + fuelCost + weightFee;
}

// Bayangkan ada 10 method lain yang tidak peduli soal fuelCost atau weightFee
public String getReceipt() {
return "Order to: " + destination;
}
}

Solusi

Pindahkan temporary fields ke class baru yang memang bertanggung jawab untuk kalkulasi tersebut.

Sebelum — temporary fields menghantui class utama
public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;

// Field ini hanya hidup selama calculateTotal()!
private double distance; // temporary
private double fuelCost; // temporary
private double weightFee; // temporary

public double calculateTotal(double weight, double km) {
distance = km;
fuelCost = distance * 2000;
weightFee = weight * 5000;
return basePrice + fuelCost + weightFee;
}
}
Sesudah — field dipindah ke class kalkulasi
public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;
// Tidak ada temporary fields di sini!

public double calculateTotal(double weight, double km) {
CalculateShippingOrder calc = new CalculateShippingOrder(weight, km);
return basePrice + calc.getTotalExtraCost();
}
}

public class CalculateShippingOrder {
public double distance;
public double fuelCost;
public double weightFee;

public CalculateShippingOrder(double weight, double km) {
this.distance = km;
this.fuelCost = distance * 2000;
this.weightFee = weight * 5000;
}

public double getTotalExtraCost() { return fuelCost + weightFee; }
}

Perbandingan

FiturSebelumSesudah
KeterbacaanField membingungkan karena hanya aktif kondisi tertentuClass hanya berisi data yang relevan permanen
PengujianHarus track side-effect antar fieldObjek kalkulasi bisa ditest terisolasi
PemeliharaanTidak jelas kapan field aman untuk dimodifikasiState sementara hilang setelah digunakan
ReusabilitasClass utama terikat pada logika kalkulasi spesifikClass kalkulasi bisa dipakai ulang di konteks lain

Refused Bequest

Subclass hanya memakai sebagian kecil warisan dari parent — sisanya diabaikan atau di-override dengan null.

Masalah

Ini melanggar Liskov Substitution Principle: subclass harus bisa menggantikan parent tanpa merusak program. Stack extends Vector artinya user bisa panggil remove(int), set(), insertElementAt() — operasi yang tidak masuk akal untuk Stack!

Explorer
refused_bequest
before
Stack.java
Main.java
after
Stack.java
Main.java
refused_bequest/before/Stack.java
package oo_abusers.refused_bequest.before;

import java.util.Vector;

public class Stack<E> extends Vector<E> {

public void push(E data) {
this.add(data);
}

public void pop() {
this.removeElementAt(this.size()-1);
}

public E peek() {
return this.elementAt(this.size()-1);
}

// Problem: tidak boleh remove by index, harus pakai pop()
// Tapi tetap bisa dipanggil dari luar karena extends Vector!
@Override
public synchronized E remove(int index) {
return null; // menolak warisan dengan diam-diam
}
}

Solusi

Ganti inheritance dengan komposisi. Stack punya Vector, bukan adalah Vector.

Sebelum — Stack extends Vector (warisan yang salah)
// Stack ADALAH Vector — user bisa akses semua method Vector
public class Stack<E> extends Vector<E> {
public void push(E data) { this.add(data); }
public void pop() { this.removeElementAt(this.size()-1); }
public E peek() { return this.elementAt(this.size()-1); }

@Override
public synchronized E remove(int index) {
return null; // menolak warisan dengan diam-diam
}
// Tapi stk.set(), stk.insertElementAt(), stk.remove(0) masih bisa dipanggil!
}
Sesudah — Stack punya Vector (komposisi)
// Stack PUNYA Vector — hanya method Stack yang terekspos
public class Stack<E> {
private Vector<E> vector = new Vector<>();

public void push(E data) { vector.add(data); }
public void pop() { vector.removeElementAt(vector.size()-1); }
public E peek() { return vector.elementAt(vector.size()-1); }
// Hanya 3 method ini yang ada. Tidak ada kejutan.
}

Perbandingan

FiturSebelumSesudah
KeterbacaanTidak jelas method mana yang "aman" dipakaiInterface class mencerminkan kemampuan nyata
PengujianPerilaku tak terduga bisa muncul dari method parentSetiap method bermakna, tidak ada yang tersembunyi
PemeliharaanOverride yang return null menyembunyikan bugTidak ada warisan yang ditolak
ReusabilitasSubclass tidak aman dipakai sebagai parentKomposisi membuat class lebih fleksibel

Alternative Classes with Different Interfaces

Dua class melakukan hal yang sama, tapi nama method-nya berbeda-beda.

Masalah

PacMan punya method draw(), sedangkan Ghost punya method paint() — padahal keduanya menggambar sprite ke layar. Code yang menggunakan keduanya terpaksa punya dua cabang logika hanya karena perbedaan nama.

Explorer
alt_classes_with_dif_interfaces
before
PacMan.java
Ghost.java
after
Drawable.java
PacMan.java
Ghost.java
alt_classes_with_dif_interfaces/before/Ghost.java
package oo_abusers.alt_classes_with_dif_interfaces.before;

import java.awt.Graphics2D;

public class Ghost {
public void paint(Graphics2D g){ // <- nama berbeda!
//draw Ghost pixel from spritesheet
}
}

Solusi

Buat interface Drawable dengan method draw(). Semua class yang bisa digambar implementasi interface ini.

Sebelum — nama method berbeda untuk hal yang sama
public class PacMan {
public void draw(Graphics2D g) { /* gambar PacMan */ }
}

public class Ghost {
public void paint(Graphics2D g) { /* gambar Ghost */ } // nama beda!
}

// Code pemanggil terpaksa handle dua cara berbeda:
// if (entity instanceof PacMan) pacman.draw(g)
// if (entity instanceof Ghost) ghost.paint(g)
Sesudah — interface menyatukan kontrak
public interface Drawable {
void draw(Graphics2D g); // kontrak tunggal
}

public class PacMan implements Drawable {
@Override public void draw(Graphics2D g) { /* gambar PacMan */ }
}

public class Ghost implements Drawable {
@Override public void draw(Graphics2D g) { /* gambar Ghost */ }
}

// Code pemanggil cukup:
// for (Drawable entity : entities) entity.draw(g);

Perbandingan

FiturSebelumSesudah
KeterbacaanDeveloper harus hafal nama method tiap classSatu interface membuat niat langsung jelas
PengujianHarus tulis test terpisah karena tidak punya tipe samaSatu test suite untuk semua Drawable
PemeliharaanTambah entitas baru = code pemanggil harus tahu nama method-nyaCukup implementasi interface
ReusabilitasLogika render terikat pada class spesifikSemua drawable bisa diperlakukan seragam